/**
 * Authors:
 * Vladimír Matěna
 * Jan Skalický
 */
package inireader;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import org.w3c.dom.NodeList;

/**
 * Class representing single property in section of configuration.
 */
public class Property
{
	private List<Element> elements;
	private Identifier name;
	private String rawName;
	private boolean isMandatory;
	private boolean isValidated;
	private ElementType type;
	private Comment comment;
	private String separator;
	private int minCount, maxCount;
	Section section;
	Restriction restriction;

	/**
	 * Creates empty property.
	 * @param name new property name
	 * @param section new property should belongs to
	 */
	public Property(Identifier name, Section section)
	{
		this.name = name;
		this.rawName = name.getId();
		
		InitializeDefault(section);
	}

	private void InitializeDefault(Section section)
	{
		this.type = ElementType.String;
		this.section = section;
		elements = new ArrayList<Element>();
		
		this.minCount = 0;
		this.maxCount = Integer.MAX_VALUE;
	}

	/**
	 * Creates new property from content and it's mandatority.
	 * @param name new property name
	 * @param isMandatory whenever should be mandatory
	 * @param value value for initial property element
	 * @param section section new property should belong to
	 */
	public Property(Identifier name, boolean isMandatory, String value,
			Section section)
	{
		this(name, section);

		this.isMandatory = isMandatory;
		elements.add(new Element(value, this));
	}

	/**
	 * Creates new property from list of strings.
	 * @param property list of string belonging to property in config file
	 * @param section section this property should belong to
	 */
	Property(List<String> property, Section section)
	{
		InitializeDefault(section);

		ListIterator<String> line = property.listIterator();

		// read comments before body - must be called before body is read
		readComment(line);

		// read property body
		if (!line.hasNext())
		{
			throw new IncorrectFormatException("Missing property body.");
		}
		String body = line.next();

		// read property name
		if (!body.contains(Configurator.ELEMENT_ASSIGN))
		{
			throw new IncorrectFormatException("Missing property name.");
		}
		int equalsPos = body.indexOf(Configurator.ELEMENT_ASSIGN);
		
		this.rawName = body.substring(0, equalsPos);
		this.name = new Identifier(Configurator.trimBlank(rawName));

		String elementsBody = body.substring(equalsPos + 1, body.length());

		// parses block with elements
		if (elementsBody.length() == 0)
		{
			throw new IncorrectFormatException("Missing elements behind '='.");
		}

		for (String element : splitElements(elementsBody))
		{
			// creates element and add comment if any
			int commentPos = element.indexOf(Configurator.COMMENT);
			if (commentPos < 0)
			{
				elements.add(new Element(element, this));
			}
			else
			{
				String elementData = element.substring(0, commentPos);
				String elementComment = element.substring(commentPos);
				Element newElement = new Element(elementData, this);
				newElement.setComment(new Comment(elementComment));
				elements.add(newElement);
			}
		}
	}

	/**
	 * Reads comment form iterator to list of lines belonging to property.
	 * @param property iterator to list of lines
	 */
	private void readComment(ListIterator<String> property)
	{
		StringBuilder commentBuffer = new StringBuilder();

		while (property.hasNext())
		{
			String curLine = property.next();

			if (curLine.trim().length() == 0 || curLine.trim().startsWith(";"))
			{
				commentBuffer.append(curLine);
				commentBuffer.append(Configurator.NEWLINE);
			}
			else
			{
				comment = new Comment(commentBuffer.toString());
				property.previous();
				break;
			}
		}
	}

	/**
	 * Gets first used separator.
	 * @param data String which is searched for separator
	 * @param separators possible separators
	 * @return found separator as string, empty string if none was found
	 */
	private String getFirstSeparator(String data, String separators)
	{
		separator = ""; // in case none is found
		for (int i = 0; i < data.length(); ++i)
		{
			if (separators.contains(String.valueOf(data.charAt(i))))
			{
				separator = String.valueOf(data.charAt(i));
				break;
			}
		}
		return separator;
	}

	/**
	 * Splits elements contained in string into list of elements.
	 * Appends comments to last.
	 * @param input source string
	 * @return list of string containing elements
	 */
	private List<String> splitElements(String input)
	{
		// split comments behind last element
		int commentPos = 0;
		while (commentPos < input.length())
		{
			String current = String.valueOf(input.charAt(commentPos));
			if (current.equals(Configurator.COMMENT))
			{
				break;
			}
			commentPos++;
		}
		String elementComment = input.substring(commentPos, input.length());
		String elementBody = input.substring(0, commentPos);

		separator = getFirstSeparator(
				elementBody,
				Configurator.ELEMENT_SEPARATORS);

		// create list of elements by separator
		List<String> split = new ArrayList<String>();
		if (separator.length() > 0)	// we have more elements
		{
			String[] separated = elementBody.split(separator);
			for (int i = 0; i < separated.length; ++i)
			{
				split.add(separated[i]);
			}
		}
		else    // just one element
		{
			split.add(elementBody);
		}

		// add comments to last element
		String last = split.get(split.size() - 1);
		split.set(split.size() - 1, last + elementComment);

		return split;
	}

	/**
	 * Validates property by corresponding part of schema.
	 * @param propElement XML element describing property
	 */
	void validateProperty(org.w3c.dom.Element propElement)
	{
		this.type = ElementType.convertFromString(propElement.getElementsByTagName("type").item(0).getTextContent());

		loadQuantity((org.w3c.dom.Element)propElement.getElementsByTagName("quantity").item(0));
		loadRestriction((org.w3c.dom.Element)propElement.getElementsByTagName("restriction").item(0));

		checkQuantity();

		this.isValidated = true;
	}

	/**
	 * Validates all elements of property.
	 */
	void validateAllElements()
	{
		for (Element element : elements)
		{
			element.validateElement();
		}
	}

	/**
	 * Loads quantity restriction from XML element.
	 * @param quantityElement XML element with restriction definition
	 */
	private void loadQuantity(org.w3c.dom.Element quantityElement)
	{
		if (quantityElement != null)
		{
			this.minCount = Integer.parseInt(quantityElement.getElementsByTagName("from").item(0).getTextContent());
			this.maxCount = Integer.parseInt(quantityElement.getElementsByTagName("to").item(0).getTextContent());
		}
	}

	/**
	 * Loads restrictions from XML element.
	 * @param restrictionElement XML element with restriction definition
	 */
	private void loadRestriction(org.w3c.dom.Element restrictionElement)
	{
		if (restrictionElement == null)
		{
			return;
		}

		if (type == ElementType.Enum)
		{
			NodeList enumNodeValues = restrictionElement.getElementsByTagName("value");

			if (enumNodeValues.getLength() == 0)
			{
				throw new ValidatingException(String.format("Enum in property %s has no value!", this.name.getId()));
			}

			List<String> enumValues = new ArrayList<String>();

			for (int i = 0; i < enumNodeValues.getLength(); i++)
			{
				enumValues.add(enumNodeValues.item(i).getTextContent());
			}

			this.restriction = new EnumRestriction(enumValues, this);
		}
		else if (type == ElementType.Int
				|| type == ElementType.UInt
				|| type == ElementType.Float)
		{
			this.restriction = new RangeRestriction(
					Double.parseDouble(restrictionElement.getElementsByTagName("from").item(0).getTextContent()),
					Double.parseDouble(restrictionElement.getElementsByTagName("to").item(0).getTextContent()),
					this);
		}
	}

	/**
	 * Checks element quantity restriction.
	 */
	private void checkQuantity()
	{
		if (elements.size() < minCount || elements.size() > maxCount)
		{
			throw new ValidatingException(String.format(
					"Property %s has unpermitted number of elements!",
					this.name.getId()));
		}
	}

	/**
	 * Gets element by index.
	 * @param index the index
	 * @return element on index, if it doesn't exist return null
	 */
	public Element getElementOnIndex(int index)
	{
		if (checkBounds(index))
		{
			return elements.get(index);
		}

		return null;
	}

	/**
	 * Gets list of elements.
	 * @return the list
	 */
	public List<Element> getElements()
	{
		return elements;
	}

	/**
	 * Get data type of elements in this property.
	 * @return type of elements
	 */
	public ElementType getType()
	{
		return type;
	}

	/**
	 * Gets property identifier.
	 * @return property identifier
	 */
	public Identifier getName()
	{
		return name;
	}

	/**
	 * Checks whenever property is mandatory.
	 * @return true if mandatory false otherwise
	 */
	public boolean isMandatory()
	{
		return isMandatory;
	}

	/**
	 * Checks whenever property was validated.
	 * @return true if validated false otherwise
	 */
	boolean isValidated()
	{
		return isValidated;
	}

	/**
	 * Adds element to property.
	 * @param element new element
	 */
	public void addElement(Element element)
	{
		element.validateElement();
		elements.add(element);
		checkQuantity();
	}

	/**
	 * Adds list of elements to property.
	 * @param elements list of new elements
	 */
	public void addElements(List<Element> elements)
	{
		for (Element element : elements)
		{
			addElement(element);
		}
	}

	/**
	 * Removes all elements from property.
	 */
	public void clearElements()
	{
		while (!elements.isEmpty())
		{
			removeElementOnIndex(0);
		}
	}

	/**
	 * Removes element with given index.
	 * @param index element index
	 * @return true if element was deleted, false otherwise
	 */
	public boolean removeElementOnIndex(int index)
	{
		if (checkBounds(index))
		{
			elements.remove(index);
			checkQuantity();
			this.section.configurator.validateAllElements();

			return true;
		}

		return false;
	}

	/**
	 * Checks whenever element with given index exists.
	 * @param index index to check
	 * @return true, if there is an element on index
	 */
	private boolean checkBounds(int index)
	{
		if (index > 0 || index < elements.size())
		{
			return true;
		}

		return false;
	}

	/**
	 * Saves section to stream.
	 * @param output the stream
	 * @throws IOException
	 */
	void save(BufferedWriter output) throws IOException
	{
		// saves comments and header
		if (comment != null)
		{
			output.write(comment.getContent());
		}
		output.write(rawName);
		output.write(Configurator.ELEMENT_ASSIGN);

		// saves elements
		boolean omitDelimiter = true;
		for (Element element : elements)
		{
			if (omitDelimiter)
			{
				omitDelimiter = false;
			}
			else
			{
				output.write(separator);
			}
			element.save(output);
		}

		output.newLine();
	}

	/**
	 * Returns string with human readable representation of property
	 *	for debugging.
	 * @return human readable representation of property
	 */
	@Override
	public String toString()
	{
		StringBuilder property = new StringBuilder();

		property.append("\tProperty:\"");
		property.append(name.toString());
		property.append("\"");
		if (comment != null)
		{
			property.append("\tCommentBefore:\"");
			property.append(comment.getContent());
			property.append("\"");
		}
		property.append(Configurator.NEWLINE);

		for (Element element : elements)
		{
			property.append(element.getElementString());
		}

		property.append(Configurator.NEWLINE);

		return property.toString();
	}

	/**
	 * Gets separator used to separate elements in this section.
	 * @return the separator
	 */
	public String getSeparator()
	{
		return separator;
	}

	/**
	 * Sets property header.
	 * @param header new header
	 */
	public final void setHeader(Identifier header)
	{
		if(section.getProperty(header.getId()) != null)
		{
			throw new ValidatingException(
							String.format("Cannot rename property to \"%s\"." +
							" Property with this name already exists!", header.getId()));
		}

		name = header;
		rawName = header.getId();

		section.configurator.validateAllElements();
	}
}//end Property

